import { NextRequest, NextResponse } from 'next/server' import { createServerSupabaseClient } from '@/lib/supabase' export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { try { const supabase = createServerSupabaseClient() const bookId = params.id // Get current user session const authHeader = request.headers.get('authorization') let user try { if (authHeader) { const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(authHeader.replace('Bearer ', '')) if (!authError) user = authUser } else { const { data: { user: sessionUser }, error: sessionError } = await supabase.auth.getUser() if (!sessionError) user = sessionUser } } catch (e) { // Ignore auth errors } if (!user) { return NextResponse.json( { error: 'Authentication required' }, { status: 401 } ) } // Get GitHub integration const { data: profile } = await supabase .from('profiles') .select('github_integrations') .eq('id', user.id) .single() const integration = profile?.github_integrations?.[bookId] if (!integration) { return NextResponse.json( { error: 'GitHub integration not found' }, { status: 404 } ) } // Get files from GitHub repository const owner = integration.github_username const repo = integration.repository_name const accessToken = integration.access_token try { // First, get the latest commit SHA to ensure we're comparing against the latest version const latestCommit = await getLatestCommit(owner, repo, accessToken) if (!latestCommit) { console.log('📊 GitHub Compare API: No commits found - repository is empty') return NextResponse.json({ committedFiles: {}, repositoryEmpty: true }) } console.log('📊 GitHub Compare API: Latest commit:', latestCommit.sha) // Get repository contents using the tree API for better performance const committedFiles = await getRepositoryFilesFromTree(owner, repo, accessToken, latestCommit.sha) console.log('📊 GitHub Compare API: Found committed files:', Object.keys(committedFiles).length) console.log('📊 GitHub Compare API: File paths:', Object.keys(committedFiles)) return NextResponse.json({ committedFiles, latestCommitSha: latestCommit.sha, repositoryEmpty: false }) } catch (githubError) { console.error('❌ GitHub Compare API: Error fetching files:', githubError) // Check if it's a 409 error (empty repository) if (githubError instanceof Error && githubError.message.includes('409')) { console.log('📊 GitHub Compare API: Repository is empty') return NextResponse.json({ committedFiles: {}, repositoryEmpty: true }) } return NextResponse.json( { error: 'Failed to fetch files from GitHub', details: githubError instanceof Error ? githubError.message : String(githubError) }, { status: 500 } ) } } catch (error) { return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } // Helper function to get the latest commit async function getLatestCommit(owner: string, repo: string, accessToken: string): Promise<{ sha: string; commit: any } | null> { try { const url = `https://api.github.com/repos/${owner}/${repo}/commits/HEAD` console.log(`📊 GitHub Compare API: Fetching latest commit from ${url}`) const response = await fetch(url, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { if (response.status === 409) { // Repository is empty return null } throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) } return await response.json() } catch (error) { console.error('❌ GitHub Compare API: Error fetching latest commit:', error) throw error } } // More efficient function using GitHub's tree API async function getRepositoryFilesFromTree(owner: string, repo: string, accessToken: string, commitSha: string): Promise<{ [path: string]: string }> { const files: { [path: string]: string } = {} try { // Get the tree for the commit (recursive to get all files) const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${commitSha}?recursive=1` console.log(`📊 GitHub Compare API: Fetching tree from ${treeUrl}`) const treeResponse = await fetch(treeUrl, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!treeResponse.ok) { throw new Error(`GitHub Tree API error: ${treeResponse.status} ${treeResponse.statusText}`) } const treeData = await treeResponse.json() console.log(`📊 GitHub Compare API: Found ${treeData.tree.length} items in tree`) // Filter for files only (not directories) and exclude README.md const fileItems = treeData.tree.filter((item: any) => item.type === 'blob' && item.path !== 'README.md' && !item.path.startsWith('.git') ) console.log(`📊 GitHub Compare API: Processing ${fileItems.length} files`) // Fetch file contents in batches to avoid rate limiting const batchSize = 10 for (let i = 0; i < fileItems.length; i += batchSize) { const batch = fileItems.slice(i, i + batchSize) await Promise.all(batch.map(async (item: any) => { try { // Use the blob API to get file content by SHA const blobUrl = `https://api.github.com/repos/${owner}/${repo}/git/blobs/${item.sha}` const blobResponse = await fetch(blobUrl, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (blobResponse.ok) { const blobData = await blobResponse.json() // GitHub returns content as base64 if it's binary or large let content = '' if (blobData.encoding === 'base64') { try { content = Buffer.from(blobData.content, 'base64').toString('utf-8') } catch { // Skip binary files console.log(`📊 GitHub Compare API: Skipping binary file ${item.path}`) return } } else { content = blobData.content } // Normalize file path to match BookWiz format const normalizedPath = normalizeFilePath(item.path) files[normalizedPath] = content console.log(`📊 GitHub Compare API: Retrieved content for ${normalizedPath} (${content.length} chars)`) } else { console.error(`❌ GitHub Compare API: Failed to fetch blob for ${item.path}:`, blobResponse.status) } } catch (fileError) { console.error(`❌ GitHub Compare API: Error fetching file ${item.path}:`, fileError) } })) } } catch (error) { console.error(`❌ GitHub Compare API: Error in getRepositoryFilesFromTree:`, error) throw error } return files } // Helper function to normalize file paths to match BookWiz format function normalizeFilePath(githubPath: string): string { // Remove any leading slashes and normalize the path const normalized = githubPath.replace(/^\/+/, '') // If the file is directly in the root, just return the filename // This matches how BookWiz stores file paths const parts = normalized.split('/') if (parts.length === 1) { return parts[0] } // For nested files, return the full path return normalized } // Keep the old function as fallback if needed async function getRepositoryFiles(owner: string, repo: string, accessToken: string, path: string = ''): Promise<{ [path: string]: string }> { const files: { [path: string]: string } = {} try { const url = `https://api.github.com/repos/${owner}/${repo}/contents${path ? `/${path}` : ''}` console.log(`📊 GitHub Compare API: Fetching ${url}`) const response = await fetch(url, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { if (response.status === 404) { // Repository might be empty or path doesn't exist console.log(`📊 GitHub Compare API: Path not found (404): ${path || 'root'}`) return files } console.error(`❌ GitHub Compare API: GitHub API error for ${url}:`, response.status, response.statusText) throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) } const contents = await response.json() // Handle single file vs array of files const items = Array.isArray(contents) ? contents : [contents] console.log(`📊 GitHub Compare API: Found ${items.length} items in ${path || 'root'}`) for (const item of items) { if (item.type === 'file' && item.name !== 'README.md') { try { // Get file content const fileResponse = await fetch(item.download_url, { headers: { 'Authorization': `Bearer ${accessToken}` } }) if (fileResponse.ok) { const content = await fileResponse.text() const filePath = path ? `${path}/${item.name}` : item.name files[filePath] = content console.log(`📊 GitHub Compare API: Retrieved content for ${filePath} (${content.length} chars)`) } else { console.error(`❌ GitHub Compare API: Failed to fetch content for ${item.name}:`, fileResponse.status) } } catch (fileError) { console.error(`❌ GitHub Compare API: Error fetching file ${item.name}:`, fileError) } } else if (item.type === 'dir') { // Recursively get directory contents const dirPath = path ? `${path}/${item.name}` : item.name console.log(`📊 GitHub Compare API: Entering directory ${dirPath}`) const dirFiles = await getRepositoryFiles(owner, repo, accessToken, dirPath) Object.assign(files, dirFiles) } } } catch (error) { console.error(`❌ GitHub Compare API: Error in getRepositoryFiles for path ${path}:`, error) throw error // Re-throw instead of silently handling } return files }